eBPF verifier验证器与编译器inline内联
笔者在二月份编写eCapture的GoTls密钥捕获功能时,遇到了一个小bug,就是在UBUNTU 21.04的Linux上运行时,遇到eBPF verifier的报错,无法启动,笔者后来通过启用always inline
解决了这个问题。
我觉得,这是一个比较好的编写eBPF的技巧案例,决定把详细排查过程、原因、修复方法一起分享出来,奈何事情比较忙,一直没时间,拖延了一个多月,这个周末,咬牙立Flag,整理出来,分享给大家。
报错详情
报错的eCapture代码是aa0b86ea[1],在ubuntu 20.04上,使用make nocore
编译运行,则一切正常。若在ubuntu 21.04以及以后版本编译运行,则出现如下错误:
tls_2023/03/24 19:44:20 EBPFProbeGoTLS BPF bytecode filename:user/bytecode/gotls_kern.o
tls_2023/03/24 19:44:21 EBPFProbeGoTLS module run failed, [skip it]. error:error:program gotls_text_register: load program: invalid argument: Arg#0 type PTR in gotls_text() is not supported yet. (2 line(s) omitted) , couldn't load eBPF programs, cs:&{map[events:PerfEventArray(keySize=0, valueSize=0, maxEntries=0, flags=0) gte_context:LRUHash(keySize=8, valueSize=4136, maxEntries=2048, flags=0) gte_context_gen:Array(keySize=4, valueSize=4136, maxEntries=1, flags=0)] map[gotls_text_register:0xc0003f6630 gotls_text_stack:0xc0003f66c0] 0xc000072f40 LittleEndian}
当然,把ubuntu 20.04上编译的版本拿到21.04上运行,也是如上错误。
关键信息
很显然,重点的错误是下面这句error:error:program gotls_text_register: load program: invalid argument: Arg#0 type PTR in gotls_text() is not supported yet. (2 line(s) omitted)
。
eCapture是使用了gojue/ebpfmanager
这个纯Go的ebpf类库来管理,里面是使用了 cilium/ebpf
类库实现syscall
eBPF调用。通过分析定位,在相应的代码处打日志,则会看到详细的报错信息,如下:
invalid argument
Validating gotls_text() func#1...
Arg#0 type PTR in gotls_text() is not supported yet.
processed 0 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0
继续跟进debug,确定报错信息来自cilium/ebpf
类库中errors
包内的323行,变量 logBuf
,当调用sys.ProgLoad
加载eBPF程序时报错。
// internal/errors.go #line 323
var err2 error
if !opts.LogDisabled && opts.LogLevel == 0 {
logBuf = make([]byte, opts.LogSize)
attr.LogLevel = LogLevelBranch
attr.LogSize = uint32(len(logBuf))
attr.LogBuf = sys.NewSlicePointer(logBuf)
_, err2 = sys.ProgLoad(attr)
}
重点的报错内容为Arg#0 type PTR in gotls_text() is not supported yet.
,而这段报错是来自内核的BPF验证器。
在内核代码中,这段报错是来自 btf_prepare_func_args[2]函数,功能为将函数的BTF转换为bpf_reg_state,无法转换BTF时,则返回EINVAL。即本次bug的现象。
该函数触发本次报错的代码如下:
/* Convert BTF function arguments into verifier types.
* Only PTR_TO_CTX and SCALAR are supported atm.
*/
for (i = 0; i < nargs; i++) {
t = btf_type_by_id(btf, args[i].type);
while (btf_type_is_modifier(t))
t = btf_type_by_id(btf, t->type);
if (btf_type_is_int(t) || btf_type_is_enum(t)) {
reg[i + 1].type = SCALAR_VALUE;
continue;
}
if (btf_type_is_ptr(t) &&
btf_get_prog_ctx_type(log, btf, t, prog_type, i)) {
reg[i + 1].type = PTR_TO_CTX;
continue;
}
bpf_log(log, "Arg#%d type %s in %s() is not supported yet.\n",
i, btf_kind_str[BTF_INFO_KIND(t->info)], tname);
return -EINVAL;
}
如注释内容,将BTF函数参数转换为验证器类型。目前仅支持PTR_TO_CTX
和SCALAR
。
问题在哪里?
很好理解,看gotls_text
这个函数代码,Arg#0
即struct pt_regs *ctx
,而这个参数实际上是PTR_TO_CTX
这个类型,但依然没通过验证器的验证。
int gotls_text(struct pt_regs *ctx, bool is_register_abi) {
// ...
}
SEC("uprobe/gotls_text_register")
int gotls_text_register(struct pt_regs *ctx) {
return gotls_text(ctx, true);
}
SEC("uprobe/gotls_text_stack")
int gotls_text_stack(struct pt_regs *ctx) {
return gotls_text(ctx, false);
}
显然,内核编译的 btf_vmlinux
是不正确的,eBPF验证器无法确定上下文指针的类型,并将struct pt_regs *ctx
作为PTR
,而非真正的PTR_TO_CTX
。
btf_vmlinux
为什么损坏,我不知道。继续排查下去会花费我很多时间、精力。所以,暂时跳过查找根因
这个方向,转向跳过验证器方法。
不过,在笔者今天写这篇文章时,发现cilium社区这两天也遇到这个问题了,在bpf: inline test functions with ctx as input #24662[3] 这个issue里,开发者也认为这是内核编译时遇到错误了,大概是pahole
或 gcc
的问题。
探索解决思路
正确的解决办法应该是定位出btf_vmlinux
损坏的原因,但这事会特别复杂、特别漫长,涉及UBUNTU
的内核编译问题排查,重现成本会很高。为此,我想尝试找到规避这个问题的方法,比如不进入BTF
的BPF verifier验证器。
为什么会被eBPF verifier验证
继续通过内核函数 btf_prepare_func_args[4] 继续跟进的话,会看到是do_check_subprogs()
函数在验证所有ebpf progs。
/* Verify all global functions in a BPF program one by one based on their BTF.
* All global functions must pass verification. Otherwise the whole program is rejected.
* Consider:
* int bar(int);
* int foo(int f)
* {
* return bar(f);
* }
* int bar(int b)
* {
* ...
* }
* foo() will be verified first for R1=any_scalar_value. During verification it
* will be assumed that bar() already verified successfully and call to bar()
* from foo() will be checked for type match only. Later bar() will be verified
* independently to check that it's safe for R1=any_scalar_value.
*/
static int do_check_subprogs(struct bpf_verifier_env *env) {
// ...
}
正如函数的注释所写,根据BTF对BPF程序中的所有全局函数逐个进行验证。所有全局函数都必须通过验证,否则整个程序将被拒绝。
在验证过程中,会先验证foo()
,验证时会假定bar()
已经通过了验证,并且只会检查从foo()
调用bar()
的类型匹配性。之后,会单独验证bar()
,以检查当R1=any_scalar_value
时是否安全。
回到我的代码写法,即gotls_text_register
调用了gotls_text
函数,所以,gotls_text
函数也会被eBPF 验证器再验证一次。
灵机一动
既然是独立的函数会被验证,那么我不拆分函数呗,但考虑到gotls_text
函数是公用部分,编写上还是要剥离出来,降低维护成本。显然,用编译器inline内联呗。
为此,我在ubuntu 20.04、21.04上分别验证了no_inline
、inline
两种模式的情况,并导出BPF字节码文件的汇编码逐一验证。
no_inline
ubuntu 20.04编译
没有自动inline,但不报错,可正常运行。
把程序放到21.04上,则报错
tls_2023/03/27 16:18:57 EBPFProbeGoTLS module run failed, [skip it]. error:error:program gotls_text_register: load program: invalid argument: Arg#0 type PTR in gotls_text() is not supported yet. (2 line(s) omitted) , couldn't load eBPF programs,
ubuntu 21.04编译
没有自动inline,但报错,运行失败
tls_2023/03/27 16:18:57 EBPFProbeGoTLS module run failed, [skip it]. error:error:program gotls_text_register: load program: invalid argument: Arg#0 type PTR in gotls_text() is not supported yet. (2 line(s) omitted) , couldn't load eBPF programs,
将21.04上编译的版本,运行报错,但放到20.04上运行,则正常。
将20.04上编译、运行正常的程序,放到21.04上,也依旧会报错。
可以确定是内核BPF验证器增加了相关验证。
见[bpf-next,3/6] bpf: Introduce function-by-function verification[5]
inline
关闭inline内联
gotls_text
关闭用inline内联
后,可以看到反汇编后的机器码:
0000000000000248 <gotls_text>:
73: bf 16 00 00 00 00 00 00 r6 = r1
74: 15 02 1f 00 00 00 00 00 if r2 == 0 goto +31 <LBB3_2>
75: b7 01 00 00 28 00 00 00 r1 = 40
76: bf 63 00 00 00 00 00 00 r3 = r6
// ...
104: 85 00 00 00 71 00 00 00 call 113
105: 05 00 1f 00 00 00 00 00 goto +31 <LBB3_3>
gotls_text_register
调用者的反汇编:
Disassembly of section uprobe/gotls_text_register:
0000000000000000 <gotls_text_register>:
0: b7 02 00 00 01 00 00 00 r2 = 1
1: 85 10 00 00 ff ff ff ff call -1
2: b7 00 00 00 00 00 00 00 r0 = 0
3: 95 00 00 00 00 00 00 00 exit
编译器自动inline内联
当gotls_text
内函数较小时,则被clang inline,比如注释掉函数后面几行。
int gotls_text(struct pt_regs *ctx, bool is_register_abi) {
s32 record_type, len;
const char *str;
void * record_type_ptr;
void * len_ptr;
record_type_ptr = (void *)go_get_argument(ctx, is_register_abi, 2);
bpf_probe_read_kernel(&record_type, sizeof(record_type), (void *)&record_type_ptr);
str = (void *)go_get_argument(ctx, is_register_abi, 3);
len_ptr = (void *)go_get_argument(ctx, is_register_abi, 4);
bpf_probe_read_kernel(&len, sizeof(len), (void *)&len_ptr);
debug_bpf_printk("gotls_text record_type:%d\n", record_type);
if (record_type != recordTypeApplicationData) {
return 0;
}
// ... 以下代码都注视掉
return 0;
}
从BPF 汇编可以看到,虽然gotls_text
函数依然存在,但是gotls_text_register
已经内联了他的内容。
llvm-objdump -d gotls_kern.o
gotls_kern.o: file format elf64-bpf
Disassembly of section .text:
0000000000000248 <gotls_text>:
73: bf 16 00 00 00 00 00 00 r6 = r1
74: 15 02 1e 00 00 00 00 00 if r2 == 0 goto +30 <LBB3_2>
75: b7 01 00 00 28 00 00 00 r1 = 40
76: bf 63 00 00 00 00 00 00 r3 = r6
77: 0f 13 00 00 00 00 00 00 r3 += r1
78: bf a1 00 00 00 00 00 00 r1 = r10
79: 07 01 00 00 f8 ff ff ff r1 += -8
80: b7 02 00 00 08 00 00 00 r2 = 8
81: 85 00 00 00 71 00 00 00 call 113
82: 79 a1 f8 ff 00 00 00 00 r1 = *(u64 *)(r10 - 8)
...
Disassembly of section uprobe/gotls_text_register:
0000000000000000 <gotls_text_register>:
0: bf 16 00 00 00 00 00 00 r6 = r1
1: b7 01 00 00 28 00 00 00 r1 = 40
2: bf 63 00 00 00 00 00 00 r3 = r6
3: 0f 13 00 00 00 00 00 00 r3 += r1
4: bf a1 00 00 00 00 00 00 r1 = r10
5: 07 01 00 00 f8 ff ff ff r1 += -8
6: b7 02 00 00 08 00 00 00 r2 = 8
7: 85 00 00 00 71 00 00 00 call 113
8: 79 a1 f8 ff 00 00 00 00 r1 = *(u64 *)(r10 - 8)
9: 7b 1a e8 ff 00 00 00 00 *(u64 *)(r10 - 24) = r1
10: bf a1 00 00 00 00 00 00 r1 = r10
11: 07 01 00 00 f4 ff ff ff r1 += -12
12: bf a3 00 00 00 00 00 00 r3 = r10
...
手动inline
可以通过__always_inline
关键字,手动让编译器对这块代码进行内联。
static __always_inline int gotls_text(struct pt_regs *ctx, bool is_register_abi) {
s32 record_type, len;
const char *str;
// ...
}
最终的反汇编情况,与 自动inline
一样,并且可以支持更多行数的代码。
解决方法
经过前面的验证,解决办法就是__always_inline
关键字启用编译器内联,跳过eBPF verifier的验证。
总结
经过笔者的验证,可以看出几个问题
ubuntu 20.04的5.4内核中,eBPF verifier未启用 对被调用子函数的参数类型判断。 ubuntu 21.04的5.11内核中,eBPF verifier启用了对被调用子函数的参数类型判断。 ubuntu 21.04的5.11内核中, btf_vmlinux
文件有问题,大概编译内核时遇到错误了。clang会对较短的子函数进行自动inline内联,具体的行数阈值我也不清楚。
编译器内联inline能很好的规避eBPF verifier的检查,建议默认开启。
其他
另外,笔者在解决这个问题时,也搜到几个类似的案例,虽然报错信息、排查思路不一样,但都是通过启用__always_inline
来解决的,供参考。
Always Use always_inline In BPF Programs[6]
CI: ebpf unit tests fail with Ubuntu 20.04.3 kernel 5.4 #24051[7]
[译] LLVM eBPF 汇编编程(2020)[8]
参考资料
aa0b86ea: https://github.com/gojue/ecapture/commit/aa0b86ea5eec189df95be0d3e99d31261fd44b51
[2]btf_prepare_func_args: https://elixir.bootlin.com/linux/v5.11/source/kernel/bpf/btf.c#L5479
[3]bpf: inline test functions with ctx as input #24662: https://github.com/cilium/cilium/pull/24662
[4]btf_prepare_func_args: https://elixir.bootlin.com/linux/v5.11/source/kernel/bpf/btf.c#L5479
[5][bpf-next,3/6] bpf: Introduce function-by-function verification: https://patchwork.ozlabs.org/project/netdev/patch/20200108072538.3359838-4-ast@kernel.org/
[6]Always Use always_inline In BPF Programs: https://hechao.li/2019/03/07/Always-Use-always_inline-In-BPF-Programs/
[7]CI: ebpf unit tests fail with Ubuntu 20.04.3 kernel 5.11 #24051: https://github.com/cilium/cilium/issues/24051
[8][译] LLVM eBPF 汇编编程(2020): https://arthurchiao.art/blog/ebpf-assembly-with-llvm-zh/